tool
extends Spatial

class_name GeneralFloor

export(Array) var mesh_data: Array setget set_mesh_data

enum PhysicsMode {
	NONE, PARENT, STATIC
}

# can only be edited in editor
export(PhysicsMode) var physics_mode: int = PhysicsMode.STATIC

export(Material) var floor_material: Material setget set_floor_material
export(Material) var decoration_material: Material setget set_decoration_material

export(PhysicsMaterial) var physics_material_override: PhysicsMaterial setget set_physics_material_override

# array of MeshInstance
# only create one in game mode, create many in edit mode
var meshes: Array = []

# array of Area
# only used in edit mode
var areas: Array = []

var _collision_object: CollisionObject = null

var _dict_editor: DictionaryEditor = null

func _init():
	if not Engine.editor_hint:
		return
	_dict_editor = DictionaryEditor.new()
	_dict_editor.properties = {
		"mesh_data": {
			"default_type": "",
			"types": [
				{
					"type": "cube_with_border",
					"params": [
						["origin", Vector3(0.0, 0.0, 0.0)],
						["extent", Vector3(2.0, 2.0, 2.0)],
						["slope", Vector2(0.0, 0.0)],
						["bottom_slope", Vector2(0.0, 0.0)],
						["border_extent", Vector3(1.0, 0.25, 1.0)],
						["border_flags", 0, {
							hint = PROPERTY_HINT_FLAGS,
							hint_string = "1,2,3,4,6,7,8,9,1 Inverted,3 Inverted,7 Inverted,9 Inverted",
						}],
						["decoration_size", 0.2],
						["decoration_flags", 0, {
							hint = PROPERTY_HINT_FLAGS,
							hint_string = "1,2,3,4,6,7,8,9",
						}],
						["face_flags", 0, {
							hint = PROPERTY_HINT_FLAGS,
							hint_string = "Hide -X,Hide +X,Hide -Z,Hide +Z,Hide -Y,Hide +Y,Remove Prism 1,Remove Prism 3,Remove Prism 7,Remove Prism 9",
						}],
					],
				},
				{
					"type": "cube",
					"params": [
						["rect", PoolVector2Array([Vector2(0.0, 0.0), Vector2(2.0, 2.0)])],
						["ys", PoolRealArray([0.0, 2.0])],
						["decoration_size", 0.2],
						["decoration_flags", 0, {
							hint = PROPERTY_HINT_FLAGS,
							hint_string = "1,2,3,4,5,6,7,8,9",
						}],
						["face_flags", 0, {
							hint = PROPERTY_HINT_FLAGS,
							hint_string = "Hide -X,Hide +X,Hide -Z,Hide +Z,Hide -Y,Hide +Y,Inverted,Remove Prish +X,Remove Prism -X",
						}],
					],
				},
				{
					"type": "rail",
					"params": [
						["origin", Vector3(0.0, 0.0, 0.0)],
						["radius", 0.14],
						["offsets", PoolVector3Array([Vector3(0.76, 0.0, 0.0), Vector3(-0.76, 0.0, 0.0)])],
						["radial_segments", 16],
						["rings_half", 4],
						["y_vector", Vector3(0.0, 1.0, 0.0)],
						["z_vector", Vector3(0.0, 0.0, 0.0)],
						["flags", 0],
						["hide_head", 0],
						["segments", [{position = Vector3(1.0, 0.0, 0.0)}]],
					],
				},
			],
		},
		"mesh_data_editor/rail/segments": {
			"default_type": "bezier",
			"types": [
				{
					"type": "bezier",
					"params": [
						["position", Vector3(0.0, 0.0, 0.0)],
						["control1", 1.0],
						["control2", 1.0],
						["offsets", PoolVector3Array()],
						["y_vector", Vector3(0.0, 0.0, 0.0)],
						["z_vector", Vector3(0.0, 0.0, 0.0)],
						["y_vector_rotate_laps", 0],
						["segment_count", 1],
						["hide_segment", 0],
						["hide_head", 0],
					],
				},
				{
					"type": "rotate",
					"params": [
						["position", Vector3(0.0, 0.0, 0.0)],
						["offsets", PoolVector3Array()],
						["y_vector", Vector3(0.0, 0.0, 0.0)],
						["z_vector", Vector3(0.0, 0.0, 0.0)],
						["y_vector_rotate_laps", 0],
						["rotate_center", Vector3(0.0, 0.0, 0.0)],
						["rotate_axis", Vector3(0.0, 1.0, 0.0)],
						["rotate_laps", 0],
						["segment_count", 1],
						["hide_segment", 0],
						["hide_head", 0],
					],
				},
			],
		},
	}
	_dict_editor.prop_root = ["mesh_data"]

func _get_property_list():
	var p: Array = []
	if Engine.editor_hint:
		_dict_editor.append_prop_list(self, p)
	return p

func _get(property):
	if Engine.editor_hint:
		return _dict_editor.get_prop(self, property)
	return null

func _set(property, value):
	if Engine.editor_hint:
		_dict_editor.set_prop(self, property, value)

func _dict_edit_action_add(prop_name: String, props: Array, editor_index: int, editor_type: String) -> int:
	var new_prop: Dictionary = {type = editor_type}

	match prop_name:
		"mesh_data":
			_add_new_geometry(new_prop)
			return mesh_data.size() - 1
		"mesh_data_editor/rail/segments":
			if editor_index < 0 or editor_index >= props.size():
				editor_index = props.size()
			else:
				editor_index += 1
			props.insert(editor_index, new_prop)
			_update_geometry_by_index(_get_editor_index())
			return editor_index

	return -1

func _dict_edit_action_remove(prop_name: String, props: Array, editor_index: int) -> void:
	match prop_name:
		"mesh_data":
			_remove_geometry(editor_index)
		"mesh_data_editor/rail/segments":
			props.remove(editor_index)
			_update_geometry_by_index(_get_editor_index())

func _dict_edit_action_duplicate(prop_name: String, props: Array, editor_index: int) -> int:
	var new_prop: Dictionary = props[editor_index].duplicate(true)

	match prop_name:
		"mesh_data":
			_add_new_geometry(new_prop)
			return mesh_data.size() - 1
		"mesh_data_editor/rail/segments":
			if editor_index < 0 or editor_index >= props.size():
				editor_index = props.size()
			else:
				editor_index += 1
			props.insert(editor_index, new_prop)
			_update_geometry_by_index(_get_editor_index())
			return editor_index

	return -1

# warning-ignore:unused_argument
# warning-ignore:unused_argument
# warning-ignore:unused_argument
# warning-ignore:unused_argument
# warning-ignore:unused_argument
# warning-ignore:unused_argument
func _dict_edit_prop_changed(prop_name: String, props: Array, editor_index: int, type: String, prop_subname: String, prop_value) -> void:
	_update_geometry_by_index(_get_editor_index())
	update_gizmo()

func _get_editor_index(prop_name: String = "mesh_data"):
	return _dict_editor.get_editor_index(self, prop_name)

func _set_editor_index(index: int, prop_name: String = "mesh_data"):
	_dict_editor.set_editor_index(self, prop_name, index)

func _editor_clicked(collider):
	for index in areas.size():
		if areas[index] == collider:
			_set_editor_index(index)
			return true
	return false

func set_physics_material_override(p: PhysicsMaterial):
	physics_material_override = p
	_update_physics()

func set_floor_material(p: Material):
	floor_material = p
	_update_material()

func set_decoration_material(p: Material):
	decoration_material = p
	_update_material()

func set_mesh_data(p: Array):
	mesh_data = p
	_update_geometry()
	if Engine.editor_hint:
		_dict_editor.set_editor_index(self, "mesh_data", -1, true, true)

static func _update_collision_shapes(co: CollisionObject, collision_shapes: Array):
	for owner_id in co.get_shape_owners():
		co.remove_shape_owner(owner_id)

	var default_owner_id: int = -1

	for cs in collision_shapes:
		if cs is Shape:
			if default_owner_id < 0:
				default_owner_id = co.create_shape_owner(co)
			co.shape_owner_add_shape(default_owner_id, cs)
		elif cs is Array:
			var owner_id = co.create_shape_owner(co)
			co.shape_owner_add_shape(owner_id, cs[0])
			co.shape_owner_set_transform(owner_id, cs[1])

func _add_new_geometry(param):
	mesh_data.append(param)

	if not Engine.editor_hint:
		push_warning("Don't call _add_new_geometry() in runtime, it's slow")
		_update_geometry()
		return

	var mi: MeshInstance = MeshInstance.new()
	meshes.append(mi)
	add_child(mi)

	# FIXME: somehow the raycast doesn't work with Area
	# so we use StaticBody instead
	var sb: StaticBody = StaticBody.new()
	sb.collision_layer = 0
	sb.collision_mask = 0
	areas.append(sb)
	add_child(sb)

	_update_geometry_by_index(mesh_data.size() - 1)

func _remove_geometry(index: int):
	if index < 0 or index >= mesh_data.size():
		return

	mesh_data.remove(index)

	if not Engine.editor_hint:
		push_warning("Don't call _remove_geometry() in runtime, it's slow")
		_update_geometry()
		return

	meshes[index].queue_free()
	areas[index].queue_free()
	meshes.remove(index)
	areas.remove(index)

func _update_geometry_by_index(index: int):
	if index < 0 or index >= mesh_data.size():
		return

	if not Engine.editor_hint:
		push_warning("Don't call _update_geometry_by_index() in runtime, it's slow")
		_update_geometry()
		return

	var mi: MeshInstance = meshes[index]
	var sb: StaticBody = areas[index]

	var st: SurfaceTool = SurfaceTool.new()
	var st2: SurfaceTool = SurfaceTool.new()
	var collision_shapes: Array = []

	st.begin(Mesh.PRIMITIVE_TRIANGLES)
	st2.begin(Mesh.PRIMITIVE_TRIANGLES)

	MyMeshBuilder.build_from_type(mesh_data[index], st, st2, collision_shapes)

	st.generate_normals()
	st.index()
	st2.generate_normals()
	st2.index()

	mi.mesh = st2.commit(st.commit())
	_update_collision_shapes(sb, collision_shapes)
	_update_material([mi])

func _update_geometry():
	if Engine.editor_hint:
		for obj in meshes:
			obj.queue_free()
		meshes.clear()

		for obj in areas:
			obj.queue_free()
		areas.clear()

		for param in mesh_data:
			var mi: MeshInstance = MeshInstance.new()
			meshes.append(mi)
			add_child(mi)

			# FIXME: somehow the raycast doesn't work with Area
			# so we use StaticBody instead
			var sb: StaticBody = StaticBody.new()
			sb.collision_layer = 0
			sb.collision_mask = 0
			areas.append(sb)
			add_child(sb)

			var st: SurfaceTool = SurfaceTool.new()
			var st2: SurfaceTool = SurfaceTool.new()
			var collision_shapes: Array = []

			st.begin(Mesh.PRIMITIVE_TRIANGLES)
			st2.begin(Mesh.PRIMITIVE_TRIANGLES)

			MyMeshBuilder.build_from_type(param, st, st2, collision_shapes)

			st.generate_normals()
			st.index()
			st2.generate_normals()
			st2.index()

			mi.mesh = st2.commit(st.commit())
			_update_collision_shapes(sb, collision_shapes)

	else:
		if meshes.size() == 0:
			return

		var st: SurfaceTool = SurfaceTool.new()
		var st2: SurfaceTool = SurfaceTool.new()
		var collision_shapes: Array = []

		st.begin(Mesh.PRIMITIVE_TRIANGLES)
		st2.begin(Mesh.PRIMITIVE_TRIANGLES)

		for param in mesh_data:
			MyMeshBuilder.build_from_type(param, st, st2, collision_shapes)

		st.generate_normals()
		st.index()
		st2.generate_normals()
		st2.index()

		meshes[0].mesh = st2.commit(st.commit())
		if _collision_object != null:
			_update_collision_shapes(_collision_object, collision_shapes)

	_update_material()
	_update_physics()

func _update_material(ms = null):
	if ms == null:
		ms = meshes
	for mi in ms:
		var mesh: Mesh = mi.mesh
		if mesh.get_surface_count() >= 1:
			mesh.surface_set_material(0, floor_material)
		if mesh.get_surface_count() >= 2:
			mesh.surface_set_material(1, floor_material if decoration_material == null else decoration_material)

func _update_physics():
	if _collision_object != null \
			and physics_mode != PhysicsMode.PARENT \
			and "physics_material_override" in _collision_object:
		_collision_object.physics_material_override = physics_material_override

func _ready():
	# FIXME: remove script-created subnodes which get duplicated by 'duplicate node'
	# this code doesn't work if inheritance is used
	for o in get_children():
		if o.owner == null:
			o.queue_free()

	if Engine.editor_hint:
		pass
	else:
		var mi: MeshInstance = MeshInstance.new()
		meshes.append(mi)
		add_child(mi)

		match physics_mode:
			PhysicsMode.PARENT:
				var p = get_parent()
				if p != null and p is CollisionObject:
					_collision_object = p
			PhysicsMode.STATIC:
				_collision_object = StaticBody.new()
				add_child(_collision_object)

	_update_geometry()
